/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.meta.util;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.time.FastDateFormat;
import org.unidata.mdm.core.type.model.ValidityPeriodElement.Granularity;
import org.unidata.mdm.meta.context.UpsertDataModelContext;
import org.unidata.mdm.meta.type.model.PeriodBoundary;
import org.unidata.mdm.meta.type.model.entities.AbstractEntity;
import org.unidata.mdm.system.util.ConvertUtils;

/**
 * @author Mikhail Mikhailov
 * Validity period utils.
 */
public class ValidityPeriodUtils {
    /**
     * Time stamp pattern (INPUT_TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS";)
     */
    private static final FastDateFormat DEFAULT_TIMESTAMP_NO_OFFSET
        = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS");

    /**
     * Globally defined validity period start.
     */
    private static AtomicReference<Date> globalValidityPeriodStart = new AtomicReference<>();
    /**
     * Globally defined validity period end.
     */
    private static AtomicReference<Date> globalValidityPeriodEnd = new AtomicReference<>();

    /**
     * Globally defined DateGranularityMode.
     */
    private static Granularity globalDateGranularityMode;
    /**
     * From comparator.
     */
    private static final Comparator<Date> FROM_COMPARATOR =
        (o1, o2) -> {

            if (o1 == null)
                return 1;

            if (o2 == null)
                return -1;

            return o2.compareTo(o1);
        };
    /**
     * To comparator.
     */
    private static final Comparator<Date> TO_COMPARATOR =
        (o1, o2) -> {

            if (o1 == null)
                return 1;

            if (o2 == null)
                return -1;

            return o1.compareTo(o2);
        };
    /**
     * Max period id.
     */
    public static final long TIMELINE_MAX_PERIOD_ID = 9223372036825200000L;

    /**
     * Constructor.
     */
    private ValidityPeriodUtils() {
        super();
    }
	/**
	 * Converts date to string representation
	 * {@see DEFAULT_TIMESTAMP_NO_OFFSET}.
	 *
	 * @param date
	 *            date to convert.
	 * @return date as string.
	 */
	public static String asString(Date date) {
		return date != null ? ValidityPeriodUtils.DEFAULT_TIMESTAMP_NO_OFFSET.format(date) : null;
	}
    /**
     * @return the globalValidityPeriodStart
     */
    public static Date getGlobalValidityPeriodStart() {
        return globalValidityPeriodStart.get();
    }
    /**
     * @return the globalValidityPeriodEnd
     */
    public static Date getGlobalValidityPeriodEnd() {
        return globalValidityPeriodEnd.get();
    }
    /**
     * @param globalValidityPeriodStart the globalValidityPeriodStart to set
     */
    public static void setGlobalValidityPeriodStart(Date start) {
        globalValidityPeriodStart.set(start);
    }
    /**
     * @param globalValidityPeriodEnd the globalValidityPeriodEnd to set
     */
    public static void setGlobalValidityPeriodEnd(Date end) {
        globalValidityPeriodEnd.set(end);
    }
    /**
     * Gets global date granularity mode.
     * @return mode
     */
    public static Granularity getGlobalDateGranularityMode() {
        return globalDateGranularityMode;
    }
    /**
     * Gets global date granularity mode.
     * @return mode
     */
    public static void setGlobalDateGranularityMode(Granularity mode) {
        globalDateGranularityMode = mode;
    }
    /**
	 * Add default validity periods to entities and lookup entities if they are
	 * not specified.
	 *
	 * @param ctx
	 *            update model request context.
	 */
	public static void adjustTimeIntervals(UpsertDataModelContext ctx) {
		adjustDefaultTimeIntervals(ctx.getEntitiesUpdate());
		adjustDefaultTimeIntervals(ctx.getLookupEntitiesUpdate());
	}

	/**
	 * Add default validity periods to entities if they are not specified.
	 *
	 * @param entities
	 *            list with entities.
	 */
	public static void adjustDefaultTimeIntervals(List<? extends AbstractEntity<?>> entities) {

	    if (CollectionUtils.isEmpty(entities)) {
			return;
		}

		entities.forEach(el -> {
			if (el.getValidityPeriod() == null) {
				el.setValidityPeriod(new PeriodBoundary()
						.withStart(ConvertUtils.date2LocalDateTime(globalValidityPeriodStart.get()))
						.withEnd(ConvertUtils.date2LocalDateTime(globalValidityPeriodEnd.get())));

			} else {
				if (el.getValidityPeriod().getStart() == null) {
					el.getValidityPeriod().setStart(ConvertUtils.date2LocalDateTime(globalValidityPeriodStart.get()));
				}
				if (el.getValidityPeriod().getEnd() == null) {
					el.getValidityPeriod().setEnd(ConvertUtils.date2LocalDateTime(globalValidityPeriodEnd.get()));
				}
			}
		});
	}

    /**
     * Returns least from date (or null for negative infinity).
     * @param fromDates the from dates
     * @return date
     */
    public static Date leastFrom(List<Date> fromDates) {

        if (fromDates != null && !fromDates.isEmpty()) {
            fromDates.sort(FROM_COMPARATOR);
            return fromDates.get(fromDates.size() - 1);
        }

        return null;
    }

    /**
     * Returns most to date (or null for positive infinity).
     * @param toDates the to dates
     * @return date
     */
    public static Date mostTo(List<Date> toDates) {

        if (toDates != null && !toDates.isEmpty()) {
            toDates.sort(TO_COMPARATOR);
            return toDates.get(toDates.size() - 1);
        }

        return null;
    }
    /**
     * The earliest date on the timeline.
     * @param d1 date one
     * @param d2 date two
     * @return the earliest date on the timeline
     */
    public static Date leastFrom(Date d1, Date d2) {

        // 1. Begin of the timeline, negative infinity
        if (d1 == null || d2 == null) {
            return null;
        }

        return d1.before(d2) ? d1 : d2;
    }
    /**
     * The latest date on the timeline.
     * @param d1 date one
     * @param d2 date two
     * @return the latest date on the timeline
     */
    public static Date mostTo(Date d1, Date d2) {

        // 1. End of the timeline, positive infinity
        if (d1 == null || d2 == null) {
            return null;
        }
        return d1.after(d2) ? d1 : d2;
    }

    public static boolean isSamePoint(Date a, Date b) {

        if (a == null && b == null) {
            return true;
        } else if (a != null && b != null) {
            return a.getTime() == b.getTime();
        }

        return false;
    }

    public static Date normalizeFrom(Date from) {

        if (Objects.isNull(from)) {
            return null;
        }

        ZonedDateTime adj;
        if (from instanceof java.sql.Date) {
            LocalDate ld = ((java.sql.Date) from).toLocalDate();
            adj = ZonedDateTime.of(ld.getYear(), ld.getMonthValue(), ld.getDayOfMonth(),
                0, 0, 0, 0, ZoneId.systemDefault());
        } else if (from instanceof java.sql.Timestamp) {
            LocalDateTime ldt = ((java.sql.Timestamp) from).toLocalDateTime();
            adj = ZonedDateTime.of(ldt.getYear(), ldt.getMonthValue(), ldt.getDayOfMonth(),
                    0, 0, 0, 0, ZoneId.systemDefault());
        } else {
            // This is probably wrong
            ZonedDateTime i = from.toInstant().atZone(ZoneId.systemDefault());
            adj = ZonedDateTime.of(i.getYear(), i.getMonthValue(), i.getDayOfMonth(),
                    0, 0, 0, 0, ZoneId.systemDefault());
        }

        return ConvertUtils.zonedDateTime2Date(adj);
    }

    public static Date normalizeTo(Date to) {

        if (Objects.isNull(to)) {
            return null;
        }

        ZonedDateTime adj;
        if (to instanceof java.sql.Date) {
            LocalDate ld = ((java.sql.Date) to).toLocalDate();
            adj = ZonedDateTime.of(ld.getYear(), ld.getMonthValue(), ld.getDayOfMonth(),
                    23, 59, 59, (int) TimeUnit.MILLISECONDS.toNanos(999), ZoneId.systemDefault());
        } else if (to instanceof java.sql.Timestamp) {
            LocalDateTime ldt = ((java.sql.Timestamp) to).toLocalDateTime();
            adj = ZonedDateTime.of(ldt.getYear(), ldt.getMonthValue(), ldt.getDayOfMonth(),
                    23, 59, 59, (int) TimeUnit.MILLISECONDS.toNanos(999), ZoneId.systemDefault());
        } else {
            // This is probably wrong
            ZonedDateTime i = to.toInstant().atZone(ZoneId.systemDefault());
            adj = ZonedDateTime.of(i.getYear(), i.getMonthValue(), i.getDayOfMonth(),
                    23, 59, 59, (int) TimeUnit.MILLISECONDS.toNanos(999), ZoneId.systemDefault());
        }

        return ConvertUtils.zonedDateTime2Date(adj);
    }
}